feat(contract): auto-expire unused launcher image hashes - #3564
feat(contract): auto-expire unused launcher image hashes#3564barakeinav1 wants to merge 41 commits into
Conversation
2c212db to
ba05472
Compare
| pub(crate) launcher_hash: LauncherImageHash, | ||
| pub(crate) compose_hashes: Vec<LauncherDockerComposeHash>, | ||
| pub(crate) added: Timestamp, | ||
| pub(crate) last_attested: Timestamp, |
There was a problem hiding this comment.
Updated (superseded by the last_used refactor in c435dc5):
last_used is a single keep-alive signal — stamped when the hash is voted in / re-voted, and refreshed on each attestation by a current participant. An entry is expired when last_used + TTL < now; the all-expired fallback keeps the most-recently-used entry.
(Originally this was two fields, added + last_attested, with expiry on max(...). Collapsed to one per Patrick's suggestion — the distinction was not load-bearing.)
There was a problem hiding this comment.
I have a feeling it's an overkill to have both in. I feel you only need the last_attested and potentially a bool to state whether it should benefit from full grace or partial one. Then this last_attested is as done currently updated along with the boolean. The expiry would be if bool=true then check last_attested<full grace period , otherwise check last_attested < partial_grace.
Of course this is assuming you have two different grace periods. If you have only one then the logic is even simpler
There was a problem hiding this comment.
Fixed already — collapsed to a single last_used in c435dc5 (we have one TTL / one grace period, so no bool needed). Sorry for the churn on this thread.
ba05472 to
b2d72fe
Compare
Launcher image hashes accumulated forever; removal required a unanimous vote. This adds usage-based expiry: - AllowedLauncherImage gains added/last_attested timestamps; an entry is expired when max(added, last_attested) + TTL < now - last_attested is refreshed on a successful attestation, but ONLY for a current participant (enforced by requiring an AuthenticatedParticipantId); a prospective/non-participant node cannot keep a stale launcher alive - reads filter out expired entries, with a newest-entry fallback so the allowed set never goes empty - verify_tee spawns a detached self-call to a new #[private] clean_expired_launcher_hashes that sweeps expired entries from storage - re-voting an existing launcher hash refreshes its added timestamp - new config launcher_hash_unused_ttl_seconds (default 14d), validated >= DEFAULT_EXPIRATION_DURATION_SECONDS - state migration (v3_12_0_state) initializes timestamps for existing entries Updates the design doc status to Implemented and regenerates the borsh-schema and ABI snapshots for the new fields/method. Closes #3381
b2d72fe to
99f6904
Compare
There was a problem hiding this comment.
Pull request overview
Implements the approved “auto-expire unused launcher image hashes” design by adding usage-based expiry for allowed launcher images in the contract, including config/ABI updates, state migration for the new borsh layout, and test coverage for refresh/expiry/cleanup behavior.
Changes:
- Add
added/last_attestedtimestamps to launcher allowlist entries and filter expired entries at read/verify time (with newest-entry fallback). - Refresh
last_attestedon successful participant submissions and add a detached private cleanup self-call to physically evict expired entries. - Introduce new config knobs (
launcher_hash_unused_ttl_seconds,clean_expired_launcher_hashes_tera_gas) with interface + DTO mapping + snapshots + migration/tests updated.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| docs/design/auto-remove-launcher-hashes-design.md | Marks the design as implemented and updates wording/invariants/decisions to match the shipped behavior. |
| crates/test-utils/src/contract_types.rs | Extends dummy config builder with the new TTL + cleanup gas fields. |
| crates/near-mpc-contract-interface/src/types/config.rs | Adds the new config fields to InitConfig/Config and updates serialization tests. |
| crates/near-mpc-contract-interface/src/method_names.rs | Adds the clean_expired_launcher_hashes method name constant. |
| crates/mpc-attestation/src/attestation.rs | Exposes the launcher compose hash from a verified attestation to support refresh-on-use. |
| crates/contract/tests/snapshots/abi__abi_has_not_changed.snap | Updates ABI snapshot for the new private cleanup method and config fields. |
| crates/contract/tests/sandbox/upgrade_from_current_contract.rs | Updates sandbox upgrade test config to include the new TTL + cleanup gas fields. |
| crates/contract/tests/sandbox/contract_configuration.rs | Updates sandbox init config test to include the new TTL + cleanup gas fields. |
| crates/contract/src/v3_12_0_state.rs | Adds 3.12.0 shadow types for launcher allowlist migration and a migration round-trip test. |
| crates/contract/src/tee/tee_state.rs | Threads launcher TTL through verification paths and adds refresh-on-use + cleanup hooks. |
| crates/contract/src/tee/proposal.rs | Implements TTL filtering, fallback selection, refresh-on-use, re-vote refresh, and cleanup for launcher allowlist entries (with tests). |
| crates/contract/src/snapshots/mpc_contract__tests__mpc_contract_borsh_schema_has_not_changed.snap | Updates borsh schema snapshot for new fields. |
| crates/contract/src/lib.rs | Wires TTL into submit/verify/read paths, spawns detached cleanup self-call, adds private cleanup endpoint, and validates config updates. |
| crates/contract/src/dto_mapping.rs | Maps new config fields between DTOs and contract config. |
| crates/contract/src/config.rs | Adds new config fields, defaults, and a validation invariant tying TTL to attestation expiry window. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| pub fn update_config(&mut self, config: dtos::Config) { | ||
| self.config = config.into(); | ||
| let new_config: Config = config.into(); | ||
| if let Err(e) = new_config.validate() { | ||
| env::panic_str(e); | ||
| } | ||
| self.config = new_config; |
There was a problem hiding this comment.
Good catch — fixed in d7c0c65. init and init_running now build the config, call config.validate(), and propagate the error (both are #[handle_result]), so the contract can no longer be initialized with a TTL below the attestation validity window. Added init_rejects_launcher_ttl_below_attestation_validity to lock it in.
Pull request overviewImplements usage-based expiry for Changes:
Reviewed changesPer-file summary
FindingsBlocking (must fix before merge):
Non-blocking (nits, follow-ups, suggestions):
|
Address review feedback: - init/init_running now validate the config (not just update_config), so the contract cannot be initialized with launcher_hash_unused_ttl_seconds below the attestation validity window; add a regression test. - dummy_config: give clean_expired_launcher_hashes_tera_gas a unique offset (was duplicating remove_non_participant_tee_verifier_votes_tera_gas).
Address non-blocking review comments: - is_expired: note why overflow returns not-expired (never panic on a bogus timestamp) - add(): note re-vote refreshes only the clock; compose hashes managed separately - refresh_launcher_usage: note the AuthenticatedParticipantId is a capability token
|
Thanks for the thorough review. Responses inline:
Fixed in d7c0c65 — both init paths now call
Done (a8694aa): added a comment explaining the deadline is unrepresentably far off and we must never panic on a bogus timestamp.
Done (a8694aa): the re-vote branch now notes compose hashes are maintained separately via
Done (a8694aa): docstring now states it is a capability token with the value intentionally unused.
Left as-is: the proposal-level
Left as-is — micro / the list is tiny, as you noted. |
…and design doc DEFAULT_EXPIRATION_DURATION_SECONDS was changed 7d -> 1d in #3626. Update the stale '7 days' wording; reference the constant instead of hardcoding a day-count in test comments to avoid future drift. The MPC docker-image grace period (DEFAULT_TEE_UPGRADE_DEADLINE_DURATION_SECONDS) remains 7 days.
Good catch — fixed in cb955fb. It was deliberately changed 7d → 1d in #3626, so the wording was stale. Updated the comments/design doc to "1 day", and to avoid this drift recurring I dropped the hardcoded day-count in the test comments (they now just reference the constant). The design-doc safety argument is now phrased around the constant ( |
| pub(crate) added: Timestamp, | ||
| pub(crate) last_attested: Timestamp, |
There was a problem hiding this comment.
Wouldn’t it be simpler to just have last_used instead of both added and last_attested? I don’t see why we need that distinction. last_used could then be refreshed both when re-voting for the launcher hash and when the attestation is used.
There was a problem hiding this comment.
Wouldn't it be simpler to just have
last_usedinstead of bothaddedandlast_attested?
Agreed — collapsed to a single last_used (refreshed on both re-vote and participant attestation) in c435dc5.
For context on why I originally split them: I wanted to distinguish governance liveness (when the hash was voted in) from usage liveness (last participant attestation), so the all-expired fallback would deliberately retain the most-recently-voted-in hash. But the only readers are the expiry check (max, unchanged) and that fallback — and "keep the most-recently-used" is just as sensible there (arguably better). So the distinction isn't load-bearing; one field is simpler and equivalent.
There was a problem hiding this comment.
Oups sorry commented the same thing. Just noticed you had the same comment
| pub(crate) added: Timestamp, | ||
| pub(crate) last_attested: Timestamp, |
There was a problem hiding this comment.
It’s a bit confusing to use the Timestamp type here while we use u64 in other places. We should probably align on one type unless there’s a specific reason not to:
mpc/crates/mpc-attestation/src/attestation.rs
Lines 206 to 208 in 187fffa
There was a problem hiding this comment.
It's a bit confusing to use the
Timestamptype here while we useu64in other places.
Kept Timestamp here: in this file it matches the sibling AllowedMpcDockerImage.added: Timestamp, and gives checked_add/now/Ord for free. The u64 you linked is in the mpc-attestation crate (raw unix seconds in the attestation DTO) — switching the launcher fields to u64 would make them inconsistent with their neighbor here. The broader u64-vs-Timestamp alignment across crates is a real but pre-existing, separate cleanup.
| /// Prepaid gas for a `remove_non_participant_tee_verifier_votes` call. | ||
| pub(crate) remove_non_participant_tee_verifier_votes_tera_gas: u64, | ||
| /// TTL after which a launcher image hash unused by any participant is evicted. | ||
| pub(crate) launcher_hash_unused_ttl_seconds: u64, |
There was a problem hiding this comment.
I see no test covering enlarging the launcher_hash_unused_ttl_seconds config while entries are stored: a bigger TTL should bring back an entry a smaller TTL had hidden, since filtering only hides (doesn't delete — only cleanup_expired does). Existing tests only advance time at a fixed TTL.
There was a problem hiding this comment.
I see no test covering enlarging the
launcher_hash_unused_ttl_seconds... a bigger TTL should bring back an entry a smaller TTL had hidden
Good point — added enlarging_ttl_unhides_previously_expired_entry in c435dc5, asserting a larger TTL re-surfaces an entry a smaller TTL hid (filtering hides, only cleanup_expired deletes).
|
The design doc [docs/design/auto-remove-launcher-hashes-design.md](https://github.com/near/mpc/blob/main/docs/design/auto-remove-launcher-hashes-design.md still says it's a Draft |
I should have written "design had been reviewed (by @pbeza and @netrome) ) |
… simplify live_indices Address review feedback: - AllowedLauncherImage: replace added + last_attested with one last_used, refreshed on both re-vote and participant attestation. Expiry and the all-expired fallback (now newest-by-last_used) are unchanged in behavior. - live_indices: drop the .expect()/empty-guard in favor of max_by_key(...).unwrap_or_default(). - Add a test that enlarging the TTL un-hides an entry a smaller TTL filtered (read-time filtering hides, never deletes). - Regenerate borsh-schema snapshot for the field change.
SimonRastikian
left a comment
There was a problem hiding this comment.
Partial review only. Will do the second part most likely later today
| // Overflow means the deadline is unrepresentably far in the future, so the | ||
| // entry is not expired. Never panic here: a bogus timestamp must not evict a hash. | ||
| None => false, |
There was a problem hiding this comment.
That's interesting, not sure if I strongly like it or strongly dislike it. I guess this should be fine and cannot be called adversarially. or can it? If it can then better evict the hash (even on bogus timestamp)
There was a problem hiding this comment.
Not adversarial: last_used is stamped by the contract via Timestamp::now() (block time, seconds) — never user-supplied — so last_used + ttl cannot approach u64::MAX for ~centuries. Overflow is unreachable in practice, so returning "not expired" is safe (and we prefer never to evict a hash on an arithmetic edge). Kept as-is with the explanatory comment.
There was a problem hiding this comment.
@barakeinav1 @SimonRastikian I think we should add a log! here. If this ever gets printed, it would be a red flag that something is very wrong. Without logging it, we might never catch it.
SimonRastikian
left a comment
There was a problem hiding this comment.
Partial review only. Will do the second part most likely later today
…c to last_used
Address review feedback (SimonRastikian):
- add -> add_or_refresh returning enum AddOutcome { Added, Refreshed }; the
bool return was always true. Thread the outcome through add_launcher_image
and make vote_add_launcher_hash log added vs refreshed. (internal only; not
in state/ABI)
- fix all_compose_hashes/launcher_hashes docstrings: they return live entries
(non-expired, or the most-recently-used fallback when all expired)
- fix stale refresh_launcher_usage docstring (last_attested -> last_used)
- rewrite the design doc to the single last_used model (struct, expiry,
mermaid, migration) and correct the stale 1-day attestation-validity wording
9cf1bd1 to
93c9ab5
Compare
|
PR title type suggestion: This PR changes only configuration files, assets, and dependencies—no source code changes. The type prefix should probably be Suggested title: |
93c9ab5 to
9cf1bd1
Compare
…aming) - TryFrom<InitConfig/Config> for Config now returns `Error` (via ConversionError::DataConversion) instead of `&'static str`, matching the other conversions in dto_mapping.rs; simplifies the init/init_running/ update_config call sites. - submit_participant_info mock arm: define refresh vars right before use and shadow the Option binding in the `if let`. - Rename AllowedLauncherImages::newest_index -> latest_expiry_index. - Name the magic timestamp/expiry constants in the expired-launcher test. - Restore the pre-existing doc on get_allowed_launcher_hashes.
Follow-up to review: the launcher-TTL rejection test lacked the GWT structure adopted across the rest of the new launcher tests.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 19 out of 19 changed files in this pull request and generated no new comments.
Suppressed comments (1)
crates/test-utils/src/contract_types.rs:23
dummy_configis intended to generate distinct values per field, butclean_expired_launcher_hashes_tera_gasis set tovalue + 14, which duplicatesverifier_tera_gas(alsovalue + 14). This can mask DTO mapping/serialization issues because swapping those fields would still produce the same dummy config.
clean_expired_launcher_hashes_tera_gas: value + 14,
…omise
Per review (gilcu3, pbeza): the MPC docker-image hashes are already cleaned
up inline in `reverify_and_cleanup_participants`, and the allowed launcher set
is a small in-memory `Vec`, so a detached-promise sweep is unnecessary
machinery. Evict expired entries inline right after the docker-hash cleanup.
Removes:
- the `#[private] clean_expired_launcher_hashes` method + its promise spawn in
`verify_tee` and the `CLEAN_EXPIRED_LAUNCHER_HASHES` method-name constant
- the `clean_expired_launcher_hashes_tera_gas` config field (Config + InitConfig
DTOs, defaults, mappings, migration shadow, test fixtures)
- `TeeState::{clean_expired_launcher_images, has_expired_launcher_images}` and
`AllowedLauncherImages::has_expired`
Regenerates the borsh-schema and ABI snapshots (only these removals). Updates
the design doc (inline eviction is now the chosen approach; the detached-promise
sweep moves to Alternatives considered).
…up_participants The eviction logic (cleanup_expired) was unit-tested in isolation, but nothing exercised it through reverify_and_cleanup_participants. Assert on raw storage (expires_at_secs) rather than the read path, which already filters expired entries and would pass regardless.
gilcu3
left a comment
There was a problem hiding this comment.
Thank you for all the fixes!
| /// The largest representable timestamp. Used as the saturating result when adding a | ||
| /// TTL to `now()` would overflow, so a bogus timestamp or enormous TTL yields an | ||
| /// entry that never expires rather than panicking. | ||
| pub(crate) const MAX: Self = Self { | ||
| duration_since_unix_epoch: Duration::MAX, | ||
| }; |
There was a problem hiding this comment.
nit: we could just drop the comment. Usually comments explaining trivial things are not useful to the reader.
| | `allowed_launcher_image_hashes()` | Returns all currently allowed launcher image hashes. | `Vec<LauncherImageHash>` | TBD | TBD | | ||
| | `allowed_launcher_compose_hashes()` | Returns all currently allowed launcher compose hashes (derived from launcher + MPC image pairs). | `Vec<LauncherDockerComposeHash>` | TBD | TBD | | ||
| | `allowed_launcher_image_hashes()` | Returns the non-expired allowed launcher image hashes (the most-recently-used entry only when all are expired). | `Vec<LauncherImageHash>` | TBD | TBD | | ||
| | `allowed_launcher_compose_hashes()` | Returns the non-expired allowed launcher compose hashes (derived from launcher + MPC image pairs; the most-recently-used entry only when all are expired). | `Vec<LauncherDockerComposeHash>` | TBD | TBD | |
There was a problem hiding this comment.
I am slightly concerned about us losing the capability of observing the expired hashes. Do we have any other way of doing so?
There was a problem hiding this comment.
Good point — filed #4047 to expose the launcher hashes' expiry off-chain (mirroring what allowed_docker_image_hashes() already does), since it's a small API decision rather than in-scope here.
|
@claude review (@barakeinav1 this is just to avoid missing something, reviews are more stringent lately) |
Pull request overviewUsage-based expiry for the launcher-image allowlist, replacing unanimous-vote-only removal for routine rotation. Each The core mechanics check out. I verified: Changes:
Reviewed changesPer-file summary
FindingsBlocking (must fix before merge):
Non-blocking (nits, follow-ups, suggestions):
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 18 changed files in this pull request and generated no new comments.
Suppressed comments (3)
docs/running-an-mpc-node-in-tdx-external-guide.md:1829
- This wording reads as if eviction from storage happens immediately once a launcher digest becomes unused. In the implementation, expiry is enforced by filtering (so it becomes unusable immediately), but storage is reclaimed later during
verify_teehousekeeping. Consider clarifying the distinction to avoid confusing operators.
An unused launcher manifest digest now auto-expires after the configured TTL (`launcher_hash_unused_ttl_seconds`, default 14 days) and is evicted automatically once no node has attested with it for that window, so no vote is needed for routine rotation. The unanimous `vote_remove_launcher_hash` is only needed to remove a still-valid digest *immediately* (before its TTL lapses), for example a compromised launcher.
crates/contract/src/tee/proposal.rs:433
- The comment says this keeps the "most-recently-used" entry, but the implementation keeps the entry with the latest
expires_at. If the configured TTL ever changes between stamps, the latestexpires_atmay not correspond to most-recently-used. Suggest rewording to match the actual selection criterion.
} else if let Some(newest) = self.latest_expiry_index() {
// All expired: keep only the most-recently-used entry.
self.entries.swap(0, newest);
self.entries.truncate(1);
docs/running-an-mpc-node-in-tdx-external-guide.md:1739
- This sentence implies the view call physically evicts expired launcher digests.
allowed_launcher_image_hashesonly filters out expired entries at read time; actual storage eviction happens during TEE housekeeping (e.g.verify_tee→reverify_and_cleanup_participants). Please reword to avoid suggesting immediate eviction on query.
This issue also appears on line 1829 of the same file.
The contract method is named `allowed_launcher_image_hashes` for historical reasons, but the values returned are manifest digests. The query returns only non-expired digests; digests that have aged out past their TTL are hidden and auto-evicted.
`update_config` became fallible in this PR (it validates the launcher TTL), but `do_update` clears all pending proposals/votes in the caller's receipt before spawning `update_config` as a separate receipt. An invalid config reaching threshold therefore wiped every other pending proposal while the config change silently failed and `vote_update` still reported success. Validate the config in `TryFrom<ProposeUpdateArgs> for Update`, so an unusable config is rejected up front and never reaches `do_update`.
- Rename PR-added tests to `<sut>__should_<assertion>` and add Given/When/Then, per the engineering standards. - Vote a launcher hash into the 3.13.0 sandbox state before upgrading, so the launcher-image migration decodes a non-empty `entries` vec off the real old layout (previously only the empty-vec path was exercised) and assert the hash survives the upgrade.
…piry - Note on `launcher_hash_unused_ttl_seconds` (config + DTO) that a change applies on an entry's next stamp, not retroactively. - Point tee-lifecycle and securing-mpc docs at the auto-removal design.
|
Thanks for the thorough review — addressed below. Blocking — invalid config proposal wipes every other pending proposal
Good catch, confirmed and fixed in 94a5fc5. Blocking — test naming / Given-When-Then
Done in ef6386d — renamed the PR-added tests to the Non-blocking — migration test only proves self-consistency / sandbox decodes an empty
Done in ef6386d — Non-blocking — config doc reads as retroactive Non-blocking — Non-blocking — no off-chain visibility into expired hashes (@gilcu3's question) Non-blocking — |
- proposal.rs: the all-expired fallback keeps the entry with the latest expiry, not the "most-recently-used" one (they differ if the TTL changed between stamps); fix the comment and local binding name. - external TDX guide: the launcher view only filters expired digests at read time; physical removal happens during routine `verify_tee`. Reword both passages so they don't read as immediate on-query eviction.
| Config(near_mpc_contract_interface::types::Config), | ||
| } | ||
|
|
||
| impl TryFrom<ProposeUpdateArgs> for Update { | ||
| type Error = Error; | ||
|
|
||
| fn try_from(value: ProposeUpdateArgs) -> Result<Self, Self::Error> { | ||
| let ProposeUpdateArgs { code, config } = value; | ||
| let update = match (code, config) { | ||
| (Some(contract), None) => Update::Contract(contract), | ||
| (None, Some(config)) => Update::Config(config), | ||
| (None, Some(config)) => { | ||
| // Reject unusable configs at proposal time: `update_config` runs in its own | ||
| // receipt, so a validation failure at apply time cannot roll back `do_update` | ||
| // (which has already cleared the pending proposals in the caller's receipt). | ||
| let _: crate::config::Config = config.clone().try_into()?; | ||
| Update::Config(config) | ||
| } |
There was a problem hiding this comment.
this really tells me that the correct change is to change the type in :
pub enum Update {
Contract(Vec<u8>),
Config(near_mpc_contract_interface::types::Config),
}
to use the internal type instead. Could be done in a follow up if you agree
Implements the approved design (docs/design/auto-remove-launcher-hashes-design.md, PR #3488).
Usage-based expiry for
allowed_launcher_image_hashes, replacing unanimous-vote-only removal for routine cleanup.Model. Each
AllowedLauncherImagestores anexpires_at: Timestamp, stampednow + TTLat write time (vote-in, re-vote, and on a successful attestation). An entry is expired whenexpires_at < now. Because expiry is fixed at write time — mirroring how attestations store their own expiry — reads are a plain comparison and the TTL only touches the write sites; it isn't threaded through the ~20 read/verify paths.expires_at = now + TTL, but only for a current participant (enforced by requiring anAuthenticatedParticipantIdcapability token), so a prospective/non-participant node cannot keep a launcher alive. Applies to both attestation paths: the mock path (MockAttestation::WithConstraintsmay reference a launcher) and the Dstack path (refresh runs in the asyncresolve_verificationcallback; the signer is preserved across the verifier promise). No node-side changes.reverify_and_cleanup_participants(the body ofverify_tee) evicts expired entries inline, right after the analogous cleanup of the MPC docker-image hashes; the allowed set is a small in-memoryVec(retain, no separate receipt) and never removes the last entry. No#[private]method, no extra config field.vote_add_launcher_hashon an already-present hash restamps itsexpires_at(threshold vote), recovering a never-adopted hash.launcher_hash_unused_ttl_seconds(default 14d), validated>= DEFAULT_EXPIRATION_DURATION_SECONDS(the attestation validity window). Validation is folded into the DTO→ConfigTryFrom, soinit/init_running/update_configcan't skip it.Migration
Shadows the live 3.13.0 baseline in
v3_13_0_state.rs:OldConfigdeserializes the old config and defaults the new fields;OldTeeStatedeserializes launcher entries without a timestamp and stampsexpires_at = migration_time + TTL. OnlyConfigandallowed_launcher_imageschanged borsh layout; every other field reuses the real (byte-identical) type. Combined withmain's #3785, the same migration also stamps an expiry on legacyMockAttestation::Validentries so they become cleanable — both steps run together and are covered by tests (including a combined round-trip).Tests
Expiry filtering, newest fallback,
cleanup_expired, re-vote refresh; expired-launcher rejection end-to-end; refresh gating (participant vs. non-participant, both mock and dstack arms); migration round-trips (launcher, legacy-mock, and combined).Follow-ups
AllowedLauncherImagesentries by hash instead of aVecInitConfigboilerplate (relax / derive fromConfig)_tera_gas→_tgasrenameCloses #3381